package ch.gibb.javamvc;

import org.apache.log4j.Logger;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;

/**
 * This class loads controller and view out of the xml, executes the controller, writes the results in the view and displays the page
 *
 * @author Bernard Jaquet
 * @version 1.0
 */
public class WebPageMapper extends HttpServlet {

    /**
     * Path of the views
     */
    private static final String VIEWS_PATH = "/WEB-INF/classes/views/";

    /**
     * Path of the mapping xml
     */
    private static final String WEB_MAP_PATH = "/WEB-INF/classes/web-app.xml";

    /**
     * Instance of the logger
     */
    private final Logger masterLogger = Logger.getLogger(WebPageMapper.class);

    /**
     * Gets the URL from the Request, gets the WebPageBean, sets all params, executes the logic in the controller, sets all output vars and parse the page with velocity
     *
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        masterLogger.info("Starting to get Site");
        String url = getURL(request);
        masterLogger.debug("Received Url of page is " + url);
        String site;
        String params;
        String demeter = String.valueOf('?');
        masterLogger.debug("Demeter is" + demeter);
        if (url.contains(demeter)) {
            site = url.split("\\?")[0];
            params = url.split("\\?")[1];
            masterLogger.debug("Parameters received: " + params);
        } else
            site = url;
        try {
            masterLogger.info("Site is " + site);
            WebPageBean page = getWebPageBeanFromURL(site);
            if (page != null) {
                response.getWriter().write(displayPage(page, request));
                masterLogger.info("Displaying page");
                return;
            }
            page = getResource(site);
            if (page != null) {
                response.getWriter().write(printResource(page));
                masterLogger.info("Printing resource");
                return;
            }
            response.getWriter().write("404 - Not Found");
        } catch (Exception e) {
            response.getWriter().write(generateErrorPage(e));
            masterLogger.error("Failed to display page because " + e.getMessage(), e);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }

    /**
     * Loads the URL from the request
     *
     * @param request
     * @return
     */
    private String getURL(HttpServletRequest request) {
        masterLogger.debug("Getting URL");
        StringBuffer url = request.getRequestURL();
        if (request.getQueryString() != null) {
            url.append('?');
            url.append(request.getQueryString());
        }
        String[] urlParts = url.toString().split("/JavaMVC-1.0/");
        return (urlParts.length > 1) ? urlParts[1] : "";
    }

    /**
     * Finds the WebPageBean witch is associated to the requested Side
     *
     * @param siteName Name of the page
     * @return The requested Bean, is null if not found
     * @throws Exception Will be thrown if the web-app.xml could not be opened or parsed in getAllPages()
     */
    private WebPageBean getWebPageBeanFromURL(String siteName) throws Exception {
        masterLogger.debug("Start getting WebPageBean");
        WebPageBean temp = new WebPageBean();
        temp.setUrl(siteName);
        List<WebPageBean> pages = getAllPages();
        if (!pages.contains(temp))
            return null;
        masterLogger.debug("Page found: " + pages.get(pages.indexOf(temp)));
        return pages.get(pages.indexOf(temp));
    }

    /**
     * Parses every single Page in the web-app.xml to a WebPageBean
     *
     * @return A list of WebPageBeans
     * @throws Exception Will be thrown if the web-app.xml could not be loaded or not be parsed
     */
    private List<WebPageBean> getAllPages() throws Exception {
        masterLogger.debug("Getting all Pages");
        List<WebPageBean> webPages = new ArrayList<WebPageBean>();
        Document doc = getWebAppXml();
        doc.getDocumentElement().normalize();
        NodeList pageContainer = doc.getElementsByTagName("pages");
        if (pageContainer.getLength() != 1)
            throw new Exception("Invalid web-app.xml: Only one pages element is allowed and necessary");
        NodeList pages = pageContainer.item(0).getChildNodes();
        for (int i = 0; i < pages.getLength(); i++) {
            if (pages.item(i).getNodeName().equals("#text"))
                continue;
            else if (!pages.item(i).getNodeName().equals("page"))
                throw new Exception("Invalid web-app.xml: There are other elements than page in the pages container");
            try {
                masterLogger.debug("xml element is: " + pages.item(i));
                WebPageBean page = new WebPageBean();
                Element element = (Element) pages.item(i);
                page.setId(element.getAttribute("id"));
                page.setUrl(element.getAttribute("url"));
                Class actionClass = Class.forName(element.getElementsByTagName("controller").item(0).getTextContent());
                //Todo: Insert Cast check with error message.
                page.setAction((ActionInterface) actionClass.newInstance());
                page.setViewPath(element.getElementsByTagName("view").item(0).getTextContent());
                masterLogger.debug("Adding new page " + page);
                webPages.add(page);
            } catch (Exception e) {
                masterLogger.error("Could not get page " + pages.item(i) + " because " + e.getMessage(), e);
                //Todo: Log event
            }
        }
        masterLogger.debug("Got all pages");
        return webPages;
    }

    private Document getWebAppXml() throws Exception {
        masterLogger.debug("Try to get web-app.xml");
        URL url = getServletContext().getResource(WEB_MAP_PATH);
        InputStream is = url.openStream();
        masterLogger.debug("web-app.xml path is: " + url.getPath());
        if (null == is)
            throw new Exception("InputStream is null");
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        return docBuilder.parse(is);
    }

    /**
     * Loads all parameters, execute the logic, get all vars with getters and generated the content of the page
     *
     * @param page WebPageBean which need to be displayed
     * @return Content of the Page
     * @throws Exception Will be thrown if something with the Veolcity went wrong or an uncatched error in the controller ocured
     */
    private String displayPage(WebPageBean page, HttpServletRequest request) {
        try {
            ActionInterface action = page.getAction();
            setParametersFromRequest(request, action);
            masterLogger.debug("Execute open page in found class");
            try {
                action.openPage();
            } catch (Exception e) {
                String msg = "An unhanded exception occurred in the controller. Reason: " + e.getMessage();
                masterLogger.error(msg, e);
                throw new Exception(msg, e);
            }
            VelocityEngine ve = getVeloctiyEngine();
            VelocityContext context = new VelocityContext();
            assignOutputVariables(action, context);
            masterLogger.debug("Getting view " + page.getViewPath());
            masterLogger.info("VMfile from url is: " + getServletContext().getResource(VIEWS_PATH + "/" + page.getViewPath()).getPath());
            Template t = ve.getTemplate(page.getViewPath());
            StringWriter writer = new StringWriter();
            t.merge(context, writer);
            return writer.toString();
        } catch (Exception e) {
            masterLogger.error("Could not generate content of WebPage because " + e.getMessage(), e);
            return generateErrorPage(e);
        }
    }

    private void setParametersFromRequest(HttpServletRequest request, ActionInterface action) throws InvocationTargetException, IllegalAccessException, IllegalArgumentException {
        masterLogger.debug("Reading parameters");
        for (String s : request.getParameterMap().keySet()) {
            try {
                Method m = new PropertyDescriptor(s, action.getClass()).getWriteMethod();
                masterLogger.debug("Setter is " + m);
                m.invoke(action, request.getParameter(s));
            } catch (IntrospectionException e) {
                masterLogger.info("No setter for parameter " + s);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("There is a type mismatch on parameter " + s, e);
            }
            masterLogger.debug("parameter " + s + " has value " + request.getParameter(s));
        }
    }

    private void assignOutputVariables(ActionInterface action, VelocityContext context) throws Exception {
        for (PropertyDescriptor pd : Introspector.getBeanInfo(action.getClass()).getPropertyDescriptors()) {
            if (pd.getReadMethod() != null && !"class".equals(pd.getName())) {
                context.put(pd.getName(), pd.getReadMethod().invoke(action));
                masterLogger.debug("Method added: " + pd.getName());
            }
        }
    }

    /**
     * Generates a VelocityEngine. Sets different logger and ResourceLoader
     *
     * @return Parametrised engine
     */
    private VelocityEngine getVeloctiyEngine() {
        masterLogger.debug("Generating VelocityEngine");
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty("runtime.log", "/var/log/velocity/velocity.log");
        ve.setProperty("resource.loader", "mine");
        ve.setProperty("mine.resource.loader.instance", new ResourceLoaderImp(this));
        return ve;
    }

    /**
     * Generates the URL to access the vm-Files.
     *
     * @param name Name of the vm-File
     * @return
     * @throws MalformedURLException
     */
    public URL getServerURL(String name) throws MalformedURLException {
        masterLogger.debug("Generating URl for view" + name);
        return getServletContext().getResource(VIEWS_PATH + name);
    }

    /**
     * Generates an Page to display the occurred error
     *
     * @param t
     * @return Content of the error page
     */
    private String generateErrorPage(Throwable t) {
        masterLogger.info("Generating error page");
        StringBuilder sb = new StringBuilder("<html><head></head><body><h1>An unhanded Error occurred</h1><br />");
        sb.append("<table><tr><td valign=\"top\" >Message: </td><td>");
        sb.append(t.getMessage());
        sb.append("</td></tr><tr><td valign=\"top\" >Cause: </td><td>");
        sb.append(t.getCause());
        sb.append("</td></tr><tr><td valign=\"top\" >Stack Trace: </td><td>");
        for (StackTraceElement element : t.getStackTrace()) {
            sb.append(element.toString());
            sb.append("<br />");
        }
        sb.append("</td></tr></table></body></html>");
        return sb.toString();
    }

    private WebPageBean getResource(String name) throws Exception {
        for (WebPageBean pageBean : getAllResources()) {
            if (pageBean.getUrl().equals(name))
                return pageBean;
        }
        return null;
    }

    private List<WebPageBean> getAllResources() throws Exception {
        List<WebPageBean> resources = new ArrayList<WebPageBean>();
        Document doc = getWebAppXml();
        NodeList resourceContainer = doc.getElementsByTagName("resources");
        if (resourceContainer.getLength() != 1)
            throw new Exception("Invalid web-app.xml: Only one resources tag is allowed.");
        NodeList allResources = resourceContainer.item(0).getChildNodes();
        for (int i = 0; i < allResources.getLength(); i++) {
            if (allResources.item(i).getNodeName().equals("#text"))
                continue;
            else if (!allResources.item(i).getNodeName().equals("resource"))
                throw new Exception("Invalid web-app.xml: There are other elements than resource in the resources container");
            try {
                WebPageBean page = new WebPageBean();
                Element element = (Element) allResources.item(i);
                page.setId(element.getAttribute("id"));
                page.setUrl(element.getAttribute("url"));
                page.setViewPath(element.getElementsByTagName("path").item(0).getTextContent());
                resources.add(page);
                masterLogger.debug("Resource added: " + page);
            } catch (Exception e) {
                masterLogger.error("Could not get resource " + allResources.item(i) + " because " + e.getMessage(), e);
            }
        }
        return resources;
    }

    private String printResource(WebPageBean resource) throws IOException {
        URLConnection connection = getServerURL(resource.getUrl()).openConnection();
        BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        StringBuilder response = new StringBuilder();
        String inputLine;
        while ((inputLine = in.readLine()) != null)
            response.append(inputLine);
        in.close();
        return response.toString();
    }
}
